/*
 * Copyright (c) 2008, Roman Bosak
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  - Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *  - Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *  - Neither the name of the DailyDevelopment nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
 * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.dailydev.jira.portal.config;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.log4j.Logger;

/**
 * Implementation of {@link IPortalPageSharingConfiguration} interface which
 * uses property file as a storage for configuration of portal page sharing.
 * 
 * @author Roman Bosak, roman.bosak@gmail.com
 */
public class PropertyPortalPageSharingConfiguration implements
        IPortalPageSharingConfiguration {

    /**
     * Logger for this class.
     */
    private static final Logger LOG = Logger
            .getLogger(PropertyPortalPageSharingConfiguration.class);

    /**
     * Classpath relative path to property file containing configuration of
     * portal page sharing. The value is used to initialize
     * {@link #configResource} field.
     */
    private static final String DEFAULT_CONFIG_RESOURCE = "dashboardSharing.properties";

    /**
     * Classpath relative path to property file containing configuration of
     * portal page sharing. Default value is set from
     * {@link #DEFAULT_CONFIG_RESOURCE} constant but can be overrided using
     * {@link #setConfigResource(String)} method.
     */
    private String configResource = DEFAULT_CONFIG_RESOURCE;

    /**
     * Map of IDs of portal pages to their sharing configurations.
     */
    private final Map<Long, ISharedPortalPage> sharedPortalPages = new HashMap<Long, ISharedPortalPage>();

    /**
     * {@link Properties} which contains parsed configuration. This instance is
     * also updated during sharing configuration management and are then stored
     * to the file.
     */
    private Properties properties;

    /**
     * Pattern to be used to parse property names to get all necessary
     * information from them.
     */
    private final Pattern propertyNamePattern = Pattern
            .compile("^portalpage\\.(.*)\\.([0-9]*)\\.group$");

    /**
     * Default constructor.
     * 
     * @throws PortalPageSharingConfigurationException
     *             can be thrown in case of problems with configuration file
     *             like can not be found or can not be changed
     */
    public PropertyPortalPageSharingConfiguration()
            throws PortalPageSharingConfigurationException {
        this.properties = new Properties();
        this.init(); // NOPMD by bosakrom on 23.4.08 18:03
    }

    /** {@inheritDoc} */
    public ISharedPortalPage addSharedPortalPage(
            final ISharedPortalPage sharedPortalPage)
            throws PortalPageSharingConfigurationException {
        String propertyName = this.getPropertyName(sharedPortalPage);
        // current value for rollback
        String currentValue = this.properties.getProperty(propertyName);

        if (LOG.isDebugEnabled()) {
            LOG.debug("Add: " + sharedPortalPage + " (" + propertyName + ")");
        }

        File configFile = getConfigFile();

        try {
            String groupName = sharedPortalPage.getGroupName();
            // group name is null when sharing to all users
            if (groupName == null) {
                groupName = "";
            }
            this.properties.put(propertyName, groupName);
            save(this.properties, configFile);
        } catch (PortalPageSharingConfigurationException e) {
            // rollback status and forward exception
            if (currentValue != null) {
                this.properties.put(propertyName, currentValue);
            } else {
                this.properties.remove(propertyName);
            }
            throw e;
        }

        // change cached configuration only if configuration save passed
        sharedPortalPages.put(sharedPortalPage.getDashboardId(),
                sharedPortalPage);

        return sharedPortalPage;
    }

    /** {@inheritDoc} */
    public Set<ISharedPortalPage> getSharedPortalPages() {
        return new HashSet<ISharedPortalPage>(sharedPortalPages.values());
    }

    /** {@inheritDoc} */
    public boolean isShared(final Long dashboardId) {
        return sharedPortalPages.containsKey(dashboardId);
    }

    /** {@inheritDoc} */
    public ISharedPortalPage removeSharedPortalPage(final Long portalPageId)
            throws PortalPageSharingConfigurationException {
        ISharedPortalPage portalPage = sharedPortalPages.remove(portalPageId);

        if (portalPage != null) {
            String propertyName = this.getPropertyName(portalPage);
            // current value for rollback
            String currentValue = this.properties.getProperty(propertyName);

            if (LOG.isDebugEnabled()) {
                LOG.debug("Remove: " + portalPage + " (" + propertyName + ")");
            }

            this.properties.remove(propertyName);

            try {
                save(this.properties, getConfigFile());
            } catch (PortalPageSharingConfigurationException e) {
                // rollback status and forward exception
                if (currentValue != null) {
                    this.properties.put(propertyName, currentValue);
                }
                sharedPortalPages.put(portalPage.getDashboardId(), portalPage);
                throw e;
            }
        }

        return portalPage;
    }

    /** {@inheritDoc} */
    public ISharedPortalPage getSharedPortalPage(final Long dashboardId) {
        return sharedPortalPages.get(dashboardId);
    }

    /**
     * Initializes instance - loads configuration property file and also in case
     * of upgrade from version 1.0 to 1.1 migrates the configuration.
     * 
     * @throws PortalPageSharingConfigurationException
     *             thrown in case of problems with configuration file like can
     *             not be found or can not be changed
     */
    protected void init() throws PortalPageSharingConfigurationException {
        load(this.properties, getConfigFile());

        if (is10Version(this.properties)) {
            this.properties = upgrade10To11(this.properties);
            save(this.properties, this.getConfigFile());
        }

        parseProperties(this.properties);
    }

    /**
     * Parses {@link Properties} specified in parameter <code>toParse</code>,
     * creates {@link SharedPortalPage} instances and registers them to
     * {@link #sharedPortalPages} {@link Map}.
     * 
     * @param toParse
     *            {@link Properties} to be parsed to instances of
     *            {@link SharedPortalPage} and registered to
     *            {@link #sharedPortalPages} {@link Map}
     */
    protected void parseProperties(final Properties toParse) {
        for (Object key : toParse.keySet()) {
            String propertyName = (String) key;

            String groupName = toParse.getProperty(propertyName);
            if (groupName.length() == 0) {
                groupName = null;
            }

            Matcher matcher = propertyNamePattern.matcher(propertyName);

            if (matcher.matches()) {
                String author = matcher.group(1);
                Long portalPageId = Long.valueOf(matcher.group(2));

                SharedPortalPage sharedDashboard = new SharedPortalPage(
                        portalPageId, author, groupName);

                this.sharedPortalPages.put(portalPageId, sharedDashboard);
            } else {
                LOG.debug("Configuration property '" + propertyName
                        + "' does not match required patter '"
                        + propertyNamePattern.toString());
            }
        }
    }

    /**
     * Checks if configuration specified by <code>toCheck</code> parameter is
     * in format used in 1.0 version of the plugin.
     * 
     * @param toCheck
     *            parsed sharing configuration to be checked if it is in format
     *            used in 1.0 version of the plugin
     * @return returns true if configuration specified by <code>toCheck</code>
     *         parameter is in format used in 1.0 version of the plugin.
     *         Otherwise false.
     */
    protected boolean is10Version(final Properties toCheck) {
        for (Object key : toCheck.keySet()) {
            String propertyName = (String) key;

            if (!propertyName.startsWith("portalpage.")) {
                return true;
            }

            // one iteration is enough - every legacy config file should contain
            // properties named 'dashboard.'
            break;
        }
        return false;
    }

    /**
     * Upgrades {@link Properties} specified by <code>toUpgrade</code>
     * parameter from format supported by version 1.0 of the plugin to format
     * supported by version 1.1 and higher.
     * 
     * @param toUpgrade
     *            {@link Properties} in legacy format supported by version 1.0
     *            of the plugin
     * @return {@link Properties} in format supported by version 1.1 and higher
     */
    protected Properties upgrade10To11(final Properties toUpgrade) {
        Properties result = new Properties();

        Set<String> configIds = new HashSet<String>();
        for (Object key : toUpgrade.keySet()) {
            String propertyName = (String) key;

            String[] nameElements = propertyName.split("\\.");
            String configId = nameElements[2];

            configIds.add(configId);
        }

        for (String configId : configIds) {
            String groupName = toUpgrade.getProperty("dashboard.share."
                    + configId + ".group");
            String author = toUpgrade.getProperty("dashboard.share." + configId
                    + ".author");
            String dashboardId = toUpgrade.getProperty("dashboard.share."
                    + configId + ".dashboardId");

            ISharedPortalPage sharedDashboard = new SharedPortalPage(Long
                    .valueOf(dashboardId), author, groupName);

            result.put(this.getPropertyName(sharedDashboard), groupName);
        }

        return result;
    }

    /**
     * Returns configuration file containing portal page sharing configuration.
     * 
     * @return configuration file containing portal page sharing configuration
     * @throws PortalPageSharingConfigurationException
     *             thrown when resource specified by {@link #configResource}
     *             field can not be found within classpath or can not be
     *             resolved to a {@link File}
     */
    protected File getConfigFile()
            throws PortalPageSharingConfigurationException {
        return getConfigFile(getResourceURL());
    }

    /**
     * Resolves and returns {@link File} for specified {@link URL}.
     * 
     * @param resourceURL
     *            {@link URL} for which the file should be resolved
     * @return {@link File} resolved for {@link URL}
     * @throws PortalPageSharingConfigurationException
     *             thrown if {@link URL} can not be resolved to a {@link File}
     */
    protected File getConfigFile(final URL resourceURL)
            throws PortalPageSharingConfigurationException {
        File configFile = null;
        try {
            configFile = new File(resourceURL.toURI());
        } catch (URISyntaxException e) {
            throw new PortalPageSharingConfigurationException(
                    "Unable to convert portal page configuration file URL ("
                            + resourceURL + ") to URI", e);
        }

        return configFile;
    }

    /**
     * Returns {@link URL} to resource (property file) containing configuration
     * of portal page sharing. Classpath relative path to resource is specified
     * by {@link #configResource} field value.
     * 
     * @return {@link URL} to resource (property file) containing configuration
     *         of portal page sharing. Classpath relative path to resource is
     *         specified by {@link #configResource} field value.
     * @throws PortalPageSharingConfigurationException
     *             thrown when resource specified by {@link #configResource}
     *             field can not be found within classpath
     */
    protected URL getResourceURL()
            throws PortalPageSharingConfigurationException {
        URL resource = PropertyPortalPageSharingConfiguration.class
                .getClassLoader().getResource(getConfigResource());
        if (resource == null) {
            throw new PortalPageSharingConfigurationException(
                    "Portal page configuration file not found> '"
                            + getConfigResource() + "'");
        }
        return resource;
    }

    /**
     * Generates name of property to be stored from passed portal page sharing
     * configuration.
     * 
     * @param sharedPortalPage
     *            portal page sharing configuration to be transformed to
     *            {@link String} representation
     * @return name of property generated from passed portal page sharing
     *         configuration
     */
    protected String getPropertyName(final ISharedPortalPage sharedPortalPage) {
        StringBuffer buffer = new StringBuffer();
        buffer.append("portalpage");
        buffer.append(".");
        buffer.append(sharedPortalPage.getAuthor());
        buffer.append(".");
        buffer.append(sharedPortalPage.getDashboardId());
        buffer.append(".");
        buffer.append("group");

        return buffer.toString();
    }

    /**
     * Loads portal page sharing configuration from file specified by
     * {@link #getConfigFile()} method into the {@link Properties} referenced by
     * {@link #properties} field.
     * 
     * @throws PortalPageSharingConfigurationException
     *             thrown in cases when it can not find the file specified or
     *             the file can not be read for some reason
     */
    protected void load() throws PortalPageSharingConfigurationException {
        this.load(this.properties, this.getConfigFile());
        this.parseProperties(this.properties);
    }

    /**
     * Loads portal page sharing configuration from file specified by parameter
     * <code>loadFile</code> into the {@link Properties} specified by
     * parameter <code>loadInto</code>.
     * 
     * @param loadInto
     *            reference to {@link Properties} into which the configuration
     *            should be loaded
     * @param loadFile
     *            file to load properties from
     * @throws PortalPageSharingConfigurationException
     *             thrown in cases when it can not find the file specified or
     *             the file can not be read for some reason
     */
    protected void load(final Properties loadInto, final File loadFile)
            throws PortalPageSharingConfigurationException {
        if (loadInto == null) {
            throw new IllegalArgumentException(
                    "Parameter 'loadInto' should not be null");
        }

        FileInputStream stream = null;
        try {
            stream = new FileInputStream(loadFile);
            loadInto.load(stream);
        } catch (FileNotFoundException e) {
            throw new PortalPageSharingConfigurationException(
                    "Unable to find portal page sharing configuration file> "
                            + loadFile.getAbsolutePath(), e);
        } catch (IOException e) {
            throw new PortalPageSharingConfigurationException(
                    "Unable to read portal page sharing configuration file> "
                            + loadFile.getAbsolutePath(), e);
        } finally {
            if (stream != null) {
                try {
                    stream.close();
                } catch (IOException e) {
                    LOG.error("Unable to close file stream> "
                            + loadFile.getAbsolutePath(), e);
                }
            }
        }
    }

    /**
     * Saves {@link Properties} passed to the method through <code>toSave</code>
     * parameter into the file defined by <code>toFile</code> parameter.
     * 
     * @param toSave
     *            reference to instance of {@link Properties} class to be stored
     * @param toFile
     *            file to store properties into
     * 
     * @throws PortalPageSharingConfigurationException
     *             thrown when configuration property file can not be found or
     *             can not be written
     */
    protected void save(final Properties toSave, final File toFile)
            throws PortalPageSharingConfigurationException {
        FileOutputStream stream = null;
        try {
            stream = new FileOutputStream(toFile);
            toSave.store(stream, "Config file updated");
        } catch (FileNotFoundException e) {
            throw new PortalPageSharingConfigurationException(
                    "Unable to find portal page sharing configuration file> "
                            + toFile.getAbsolutePath(), e);
        } catch (IOException e) {
            throw new PortalPageSharingConfigurationException(
                    "Unable to read portal page sharing configuration file> "
                            + toFile.getAbsolutePath(), e);
        } finally {
            if (stream != null) {
                try {
                    stream.close();
                } catch (IOException e) {
                    LOG.error("Unable to close file stream> "
                            + toFile.getAbsolutePath(), e);
                }
            }
        }
    }

    /**
     * Returns classpath relative path to property file containing configuration
     * of portal page sharing.
     * 
     * @return classpath relative path to property file containing configuration
     *         of portal page sharing
     */
    public String getConfigResource() {
        return configResource;
    }

    /**
     * Sets classpath relative path to property file containing configuration of
     * portal page sharing.
     * 
     * @param configPath
     *            classpath relative path to property file containing
     *            configuration of portal page sharing
     */
    public void setConfigResource(final String configPath) {
        this.configResource = configPath;
    }

}
